fuzz: add fuzz target for P2PGossipSync gossip message handling#4532
fuzz: add fuzz target for P2PGossipSync gossip message handling#4532NishantBansal2003 wants to merge 2 commits intolightningdevkit:mainfrom
Conversation
|
👋 Thanks for assigning @TheBlueMatt as a reviewer! |
Signed-off-by: Nishant Bansal <nishant.bansal.282003@gmail.com>
cb9e2a8 to
d29150d
Compare
fuzz/src/gossip_discovery.rs
Outdated
|
|
||
| let graph = network_graph.read_only(); | ||
| let node_id = NodeId::from_pubkey(&peer.node_id); | ||
| let node = graph.node(&node_id).unwrap(); | ||
| let info = node.announcement_info.as_ref().unwrap(); | ||
| assert_eq!(info.last_update(), timestamp); |
There was a problem hiding this comment.
The Ok branch assertions look up peer.node_id (the original, pre-mutation value), but if the node_id was mutated at line 184-186, the accepted announcement would be stored under ann.contents.node_id (the mutated value). In secp256k1_fuzz mode, signature verification is simplified and could theoretically allow a mutated node_id to pass, causing graph.node(&NodeId::from_pubkey(&peer.node_id)).unwrap() to panic on a node that was never updated.
To be robust regardless of fuzz-mode crypto behavior, this should use the actual message values:
| let graph = network_graph.read_only(); | |
| let node_id = NodeId::from_pubkey(&peer.node_id); | |
| let node = graph.node(&node_id).unwrap(); | |
| let info = node.announcement_info.as_ref().unwrap(); | |
| assert_eq!(info.last_update(), timestamp); | |
| let graph = network_graph.read_only(); | |
| let node = graph.node(&ann.contents.node_id).unwrap(); | |
| let info = node.announcement_info.as_ref().unwrap(); | |
| assert_eq!(info.last_update(), timestamp); |
fuzz/src/gossip_discovery.rs
Outdated
| let graph = network_graph.read_only(); | ||
| let chan = graph.channel(scid).unwrap(); | ||
| assert_eq!(chan.node_one, NodeId::from_pubkey(&peer1.node_id)); | ||
| assert_eq!(chan.node_two, NodeId::from_pubkey(&peer2.node_id)); | ||
|
|
||
| let node1_id = NodeId::from_pubkey(&peer1.node_id); | ||
| let node2_id = NodeId::from_pubkey(&peer2.node_id); | ||
| assert!(graph.node(&node1_id).is_some()); | ||
| assert!(graph.node(&node2_id).is_some()); |
There was a problem hiding this comment.
Same issue as the node announcement case: these assertions use the original peer1.node_id/peer2.node_id, but the node_id_1/node_id_2 fields may have been mutated at lines 238-246. If a mutation passes signature verification in secp256k1_fuzz mode, chan.node_one/chan.node_two would hold the mutated values, causing the assert_eq! to fail spuriously.
Use the actual announcement contents instead:
| let graph = network_graph.read_only(); | |
| let chan = graph.channel(scid).unwrap(); | |
| assert_eq!(chan.node_one, NodeId::from_pubkey(&peer1.node_id)); | |
| assert_eq!(chan.node_two, NodeId::from_pubkey(&peer2.node_id)); | |
| let node1_id = NodeId::from_pubkey(&peer1.node_id); | |
| let node2_id = NodeId::from_pubkey(&peer2.node_id); | |
| assert!(graph.node(&node1_id).is_some()); | |
| assert!(graph.node(&node2_id).is_some()); | |
| let graph = network_graph.read_only(); | |
| let chan = graph.channel(scid).unwrap(); | |
| assert_eq!(chan.node_one, ann.contents.node_id_1); | |
| assert_eq!(chan.node_two, ann.contents.node_id_2); | |
| assert!(graph.node(&ann.contents.node_id_1).is_some()); | |
| assert!(graph.node(&ann.contents.node_id_2).is_some()); |
|
I've thoroughly reviewed every file and hunk in this diff. All three issues from the prior review have been addressed:
No new issues found. The fuzz target logic is sound: the |
| @@ -0,0 +1,462 @@ | |||
| //! Test that no series of gossip messages received from peers can result in a crash. We do this | |||
There was a problem hiding this comment.
Nit: This file is missing the standard licensing header that other fuzz targets (e.g., full_stack.rs, chanmon_consistency.rs, feature_flags.rs) include at the top:
// This file is Copyright its original authors, visible in version control
// history.
//
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.
The doc comment (//! Test that ...) should come after this header.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4532 +/- ##
=======================================
Coverage 87.10% 87.11%
=======================================
Files 163 163
Lines 108740 108740
Branches 108740 108740
=======================================
+ Hits 94723 94730 +7
+ Misses 11531 11526 -5
+ Partials 2486 2484 -2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
TheBlueMatt
left a comment
There was a problem hiding this comment.
Cool, thanks! Have you run any comparisons on the coverage of this vs the router fuzzer? I'm wondering if it makes sense to add a new fuzzer here or focus more CPU on the router fuzzer given it tests very similar codepaths.
Coverage wise, the additional benefit is that P2PGossipSync also gets covered, so that's a plus. My main goal was to cover all announcement messages in the fuzz tests (which the One option is to extend the OR, we can keep a separate fuzz target ( WDYT? I'm fine with either approach since both will ultimately achieve full coverage of gossip message processing. |
|
🔔 1st Reminder Hey @joostjager! This PR has been waiting for your review. |
|
🔔 2nd Reminder Hey @joostjager! This PR has been waiting for your review. |
TheBlueMatt
left a comment
There was a problem hiding this comment.
Hmm, yea, I suppose the goal is sufficiently different that it makes sense to just ship a separate fuzzer. What could go wrong, anyway? :)
fuzz/src/gossip_discovery.rs
Outdated
| excess_data, | ||
| }; | ||
|
|
||
| if mutate_field!() { |
There was a problem hiding this comment.
I'm not entirely sure its worth all the effort for creative message building - should we instead just read a length and then deserialize N bytes as each message and process it?
There was a problem hiding this comment.
I followed your suggestion and used a decode method to fill the inputs (mostly taken from router.rs). The reason for doing this is that the code is now somewhat reduced. But, the downside is that some fuzz input gets wasted due to being overwritten. I think it’s fine?
The reason I initially did manual field parsing is i want the fuzz tests to cover P2P gossip sync e2e, including sign verify and raw fuzz input won't penetrate sign verification, so we need to define some valid fields manually
There was a problem hiding this comment.
including sign verify and raw fuzz input won't penetrate sign verification
We deliberately use broken signatures and hash function in fuzzing so the fuzzer should be able to do this, even if its a bit of work (usually just all-0s and a single byte).
There was a problem hiding this comment.
Great, I saw the code in rust-secp256k1 and noticed that with the secp256k1_fuzz flag, there is a fuzzed implementation of secp256k1_ecdsa_verify (https://github.com/rust-bitcoin/rust-secp256k1/blob/master/secp256k1-sys/src/lib.rs#L1828-L1845). So, I think it's fine to remove the manual message building. Working on it now
Added gossip state machine fuzz tests for gossip discovery state handling. In these fuzz tests, we read bytes from the fuzz input to determine actions such as connecting peers, feeding channel announcements, node announcements, channel updates, or query messages. Both valid and malformed messages are generated to exercise error paths.
The reason for adding these fuzz tests is that
P2PGossipSyncis the ultimate sink for gossip messages received from peers, so there must not be any crashes or unintended behavior in LDK when handling messages from potentially malicious peers.I only included the states that LDK currently handles. For example, I did not add
reply_channel_rangeorquery_scidsmessages since LDK does not process them at the moment. Similarly, sinceP2PGossipSyncdoes not handlegossip_timestamp_filter, it is not included in these fuzz tests. Additionally, there is limited benefit in fuzzinggossip_timestamp_filterin LDK, as it is typically processed only once per peer and involves minimal logical processing.